﻿using Red.Controls.Shell;
using Red.Toolkit.EngineLogsViewer.Data;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows;

namespace Red.Toolkit.EngineLogsViewer
{
    #region Helper class for memory stream

    public class QueueStream : MemoryStream
    {
        public long ReadPosition;
        public long WritePosition;

        public QueueStream() : base() { }

        public QueueStream( byte[] buffer ) : base()
        {
            Write( buffer, 0, buffer.Length );
        }

        public override int Read( byte[] buffer, int offset, int count )
        {
            Position = ReadPosition;

            var temp = base.Read( buffer, offset, count );

            ReadPosition = Position;

            return temp;
        }

        public override void Write( byte[] buffer, int offset, int count )
        {
            Position = WritePosition;

            base.Write( buffer, offset, count );

            WritePosition = Position;
        }
    }

    #endregion

    #region Enums

    public enum ControlCode
    {
        Empty,

        GiveMeSpamLogs,
        BlockSpamLogs,
    }

    #endregion

    #region Interfaces

    public interface IListenerInterface
    {
        void OnReceiveData( byte[] data, int size );
    }

    #endregion

    public class ConnectionInfo
    {
        public Socket AssignedSocket;
        public IListenerInterface AssignedCallback;
        public Action<bool> ConnectionCallback;

        public byte[] ReceiveBuffer = new byte[1024];
    }

    public class ConnectionManager : IListenerInterface
    {
        #region Data

        private string _connectionAddress = "127.0.0.1";
        private string _connectionMessage = "Please enter launcher IP address:";

        RedEditorWindow _parentWindow;
        EngineLogsViewerViewModel _modelView;
        Action<bool> _connectionCallback;

        Socket _client;

        QueueStream _receivedData = new QueueStream();
        int _packetSize = 0;

        const int _defaultConnectionPort = 3456;

        #endregion

        #region Constructor

        public ConnectionManager( RedEditorWindow parentWindow )
        {
            _parentWindow = parentWindow;
            _modelView = (EngineLogsViewerViewModel)_parentWindow.DataContext;
        }

        #endregion

        #region Public Methods

        public void Reconnect( Action<bool> connectionCallback )
        {
            EndConnection();
            TryToConnectWithLauncher( connectionCallback );
        }

        public void TryToConnectWithLauncher( Action<bool> connectionCallback )
        {
            _connectionCallback = connectionCallback;

            IPAddress ipAddress;
            int port = _defaultConnectionPort;
            bool parseResult = false;

            do
            {
                _connectionAddress = TextInputDialog.Show( _parentWindow, _connectionAddress, _connectionMessage, "Launcher IP address" );

                if ( _connectionAddress == null )    // user canceled operation
                {
                    _modelView.IsConnected = false;
                    _connectionAddress = "127.0.0.1";
                    return;
                }

                string hostName = "";
                ParseHostString( _connectionAddress, ref hostName, ref port );
                parseResult = IPAddress.TryParse( hostName, out ipAddress );
                if ( parseResult == false )
                    _connectionMessage = "Incorrect IP address syntax.\n\nPlease enter launcher IP address:";

            } while ( parseResult == false );

            StartClient( ipAddress, port, connectionCallback );
        }

        public void ParseHostString( string hostString, ref string hostName, ref int port )
        {
           hostName = hostString;
           if( hostString.Contains( ":" ) )
           {
              string[] hostParts = hostString.Split(':');

              if( hostParts.Length == 2 )
              {
                 hostName = hostParts[0];
                 int.TryParse(hostParts[1], out port);
              }
           }
        }

        public void EndConnection()
        {
            if ( _client != null && _client.Connected )
            {
                _client.Shutdown( SocketShutdown.Both );
                _modelView.IsConnected = false;
                _connectionCallback = null;
            }
        }

        public void SendMessage( ControlCode controlCode )
        {
            const int magicNumber = 0x12345678;

            MemoryStream ms = new MemoryStream();
            BinaryWriter bw = new BinaryWriter( ms );

            bw.Write( magicNumber );
            bw.Write( (int)controlCode );

            _client.Send( ms.GetBuffer(), (int)ms.Length, SocketFlags.None );
        }

        #endregion

        #region IListenerInterface implementation

        public void OnReceiveData( byte[] data, int size )
        {
            try
            {
                BinaryWriter bw = new BinaryWriter( _receivedData );
                bw.Write( data, 0, size );

                if ( _packetSize == 0 && _receivedData.Length > 8 )
                {
                    BinaryReader br = new BinaryReader( _receivedData );
                    _packetSize = br.ReadInt32();
                }

                if ( _packetSize != 0 && _receivedData.Length >= _packetSize )
                {
                    BinaryReader br = new BinaryReader( _receivedData );
                    var newEntryDateTime = new DateTime( 1970, 1, 1 ).AddSeconds( br.ReadInt64() ).AddHours( 2.0 ); // add 2.0 because is
                    var newEntryThreadId = br.ReadUInt32();
                    var newEntrySeverity = br.ReadUInt32();
                    var logText = Encoding.ASCII.GetString( br.ReadBytes( _packetSize - 16 ) ); // 16 is sum of timestamp size, thread id size, severity size and type size

                    _modelView.AddNewLog( newEntryDateTime, newEntryThreadId, newEntrySeverity, logText );

                    var localData = _receivedData.ToArray().Skip( (int)_receivedData.ReadPosition ).ToArray();
                    _receivedData = new QueueStream( localData );
                    _packetSize = 0;
                }
            }
            catch ( System.Exception ex )
            {
                Console.WriteLine( ex.Message );
            }
        }

        #endregion

        #region Private Methods

        private void StartClient( IPAddress ipAddress, int port, Action<bool> connectionCallback )
        {
            try
            {
                _client = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );

                ConnectionInfo connection = new ConnectionInfo();
                connection.AssignedSocket = _client;
                connection.AssignedCallback = this;
                connection.ConnectionCallback = connectionCallback;

                _client.BeginConnect( ipAddress, port, ConnectCallback, connection );
            }
            catch ( Exception ex )
            {
                connectionCallback( false );
                Console.WriteLine( "Creating connection: " + ex.Message );
                return;
            }
        }

        private void ConnectCallback( IAsyncResult ar )
        {
            try
            {
                ConnectionInfo connection = (ConnectionInfo)ar.AsyncState;

                if ( connection.AssignedSocket.Connected == false )
                {
                    _modelView.IsConnected = false;
                    connection.ConnectionCallback( false );

                    Application.Current.Dispatcher.Invoke( delegate
                    {
                        _connectionMessage = $"Viewer cannot connect with IP: {_connectionAddress}. \n\nPlease enter new launcher IP address:";
                        TryToConnectWithLauncher( _connectionCallback );
                    } );
                }
                else
                {
                    _modelView.IsConnected = true;
                    connection.ConnectionCallback( true );

                    connection.AssignedSocket.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true );
                    connection.AssignedSocket.Blocking = false;

                    connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, 0, connection.ReceiveBuffer.Length, SocketFlags.None, new AsyncCallback( ReceiveCallback ), connection );
                }
            }
            catch ( Exception e )
            {
                Console.WriteLine( e.ToString() );
            }
        }

        private void ReceiveCallback( IAsyncResult ar )
        {
            ConnectionInfo connection = (ConnectionInfo)ar.AsyncState;

            int bytesRead = 0;
            try
            {
                bytesRead = connection.AssignedSocket.EndReceive( ar );
            }
            catch ( SocketException ex )
            {
                Console.WriteLine( "ReadCallback: " + ex.Message );
                return;
            }

            if ( bytesRead > 0 )
            {
                connection.AssignedCallback.OnReceiveData( connection.ReceiveBuffer, bytesRead );
            }

            try
            {
                connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, 0, connection.ReceiveBuffer.Length, SocketFlags.None, new AsyncCallback( ReceiveCallback ), connection );
            }
            catch ( SocketException ex )
            {
                if ( ex.SocketErrorCode == SocketError.Shutdown || ex.SocketErrorCode == SocketError.ConnectionAborted || ex.SocketErrorCode == SocketError.ConnectionReset )
                {
                    Console.WriteLine( "Connection was closed." );
                    connection.AssignedSocket.Close();
                }
            }
        }

        #endregion
    }
}
